﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.WebPages;

namespace Mefisto4.Core.MVC
{
    [Export(typeof(IViewEngine))]
    public class M4ViewEngine : RazorViewEngine
    {
        // format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:"
        private static readonly string ViewStartFileName = "_ViewStart";
        private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}:";
        private const string CacheKeyPrefixMaster = "Master";
        private const string CacheKeyPrefixPartial = "Partial";
        private const string CacheKeyPrefixView = "View";
        private static readonly string[] _emptyLocations = new string[0];
        private DisplayModeProvider _displayModeProvider;

        private VirtualPathProvider _vpp;
        internal Func<string, string> GetExtensionThunk = VirtualPathUtility.GetExtension;

        protected M4ViewEngine()
        {
            ViewLocationCache = HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled
                                    ? DefaultViewLocationCache.Null
                                    : new DefaultViewLocationCache();

            AreaViewLocationFormats = new[]
            {
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            AreaMasterLocationFormats = new[]
            {
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            AreaPartialViewLocationFormats = new[]
            {
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Modules/{3}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            ViewLocationFormats = new[]
            {
                "~/Modules/{3}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Views/{1}/{0}.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            MasterLocationFormats = new[]
            {
                "~/Modules/{3}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Views/{1}/{0}.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            PartialViewLocationFormats = new[]
            {
                "~/Modules/{3}/Views/{1}/{0}.cshtml",
                "~/Modules/{3}/Views/{1}/{0}.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            FileExtensions = new[]
            {
                "cshtml",
                "vbhtml",
            };
        }

        private string CreateCacheKey(string prefix, string name, string controllerName, string areaName, string module = "")
        {
            return String.Format(CultureInfo.InvariantCulture, CacheKeyFormat, GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName, module);
        }

        internal static string AppendDisplayModeToCacheKey(string cacheKey, string displayMode)
        {
            // key format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:{module}:"
            // so append "{displayMode}:" to the key
            return cacheKey + displayMode + ":";
        }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            if (String.IsNullOrEmpty(partialViewName))
                throw new ArgumentException("partialViewName cannot be null or empty");

            string[] searched;

            var controllerName = controllerContext.RouteData.Values["controller"].ToString();
            var moduleName = controllerContext.RouteData.Values["module"].ToString();
            var partialPath = GetPath(controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, moduleName, CacheKeyPrefixPartial, useCache, out searched);

            return String.IsNullOrEmpty(partialPath) ? new ViewEngineResult(searched) : new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            if (String.IsNullOrEmpty(viewName))
                throw new ArgumentException("viewName cannot be null or empty");

            string[] viewLocationsSearched;
            string[] masterLocationsSearched;

            var controllerName = controllerContext.RouteData.Values["controller"].ToString();
            var moduleName = controllerContext.RouteData.Values["module"].ToString();
            var viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, moduleName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
            var masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, moduleName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);

            if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
                return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));

            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
        }

        private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string moduleName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
        {
            searchedLocations = _emptyLocations;

            if (String.IsNullOrEmpty(name))
                return String.Empty;

            var areaName = AreaHelpers.GetAreaName(controllerContext.RouteData);
            var usingAreas = !String.IsNullOrEmpty(areaName);
            var viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null);

            if (viewLocations.Count == 0)
                throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "{0} cannot be null or empty", locationsPropertyName));

            var nameRepresentsPath = IsSpecificPath(name);
            var cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName, moduleName);

            if (!useCache)
            {
                return nameRepresentsPath
                           ? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations)
                           : GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, moduleName, cacheKey, ref searchedLocations);
            }

            // Only look at cached display modes that can handle the context.
            var possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode);
            foreach (var displayMode in possibleDisplayModes)
            {
                var cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId));

                if (cachedLocation == null)
                    continue;

                if (controllerContext.DisplayMode == null)
                    controllerContext.DisplayMode = displayMode;

                return cachedLocation;
            }

            // GetPath is called again without using the cache.
            return null;
        }

        private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string moduleName, string cacheKey, ref string[] searchedLocations)
        {
            var result = String.Empty;
            searchedLocations = new string[locations.Count];

            for (var i = 0; i < locations.Count; i++)
            {
                var location = locations[i];
                var virtualPath = location.Format(name, controllerName, areaName, moduleName);
                var virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode);

                if (virtualPathDisplayInfo != null)
                {
                    var resolvedVirtualPath = virtualPathDisplayInfo.FilePath;

                    searchedLocations = _emptyLocations;
                    result = resolvedVirtualPath;
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, virtualPathDisplayInfo.DisplayMode.DisplayModeId), result);

                    if (controllerContext.DisplayMode == null)
                        controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode;

                    // Populate the cache with the existing paths returned by all display modes.
                    // Since we currently don't keep track of cache misses, if we cache view.aspx on a request from a standard browser
                    // we don't want a cache hit for view.aspx from a mobile browser so we populate the cache with view.Mobile.aspx.
                    var allDisplayModes = DisplayModeProvider.Modes;
                    foreach (var displayMode in allDisplayModes)
                    {
                        if (displayMode.DisplayModeId == virtualPathDisplayInfo.DisplayMode.DisplayModeId)
                            continue;

                        var displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path));

                        if (displayInfoToCache != null && displayInfoToCache.FilePath != null)
                            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayInfoToCache.DisplayMode.DisplayModeId), displayInfoToCache.FilePath);
                    }
                    break;
                }

                searchedLocations[i] = virtualPath;
            }

            return result;
        }

        private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
        {
            var result = name;

            if (!(FilePathIsSupported(name) && FileExists(controllerContext, name)))
            {
                result = String.Empty;
                searchedLocations = new[] { name };
            }

            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
            return result;
        }

        private bool FilePathIsSupported(string virtualPath)
        {
            if (FileExtensions == null)
                // legacy behavior for custom ViewEngine that might not set the FileExtensions property
                return true;

            // get rid of the '.' because the FileExtensions property expects extensions withouth a dot.
            var extension = GetExtensionThunk(virtualPath).TrimStart('.');
            return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
        }

        private static List<ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats)
        {
            var allLocations = new List<ViewLocation>();

            if (areaViewLocationFormats != null)
            {
                foreach (var areaViewLocationFormat in areaViewLocationFormats)
                {
                    allLocations.Add(new AreaAwareViewLocation(areaViewLocationFormat));
                }
            }

            if (viewLocationFormats != null)
            {
                foreach (var viewLocationFormat in viewLocationFormats)
                {
                    allLocations.Add(new ViewLocation(viewLocationFormat));
                }
            }

            return allLocations;
        }

        private static bool IsSpecificPath(string name)
        {
            var c = name[0];
            return (c == '~' || c == '/');
        }

        #region Classes

        private class AreaAwareViewLocation : ViewLocation
        {
            public AreaAwareViewLocation(string virtualPathFormatString)
                : base(virtualPathFormatString)
            {
            }

            public override string Format(string viewName, string controllerName, string areaName, string moduleName)
            {
                return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, moduleName);
            }
        }

        private class ViewLocation
        {
            protected string _virtualPathFormatString;

            public ViewLocation(string virtualPathFormatString)
            {
                _virtualPathFormatString = virtualPathFormatString;
            }

            public virtual string Format(string viewName, string controllerName, string areaName, string moduleName)
            {
                return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, moduleName);
            }
        }

        internal static class AreaHelpers
        {
            public static string GetAreaName(RouteBase route)
            {
                var routeWithArea = route as IRouteWithArea;
                if (routeWithArea != null)
                    return routeWithArea.Area;

                var castRoute = route as Route;
                if (castRoute != null && castRoute.DataTokens != null)
                    return castRoute.DataTokens["area"] as string;

                return null;
            }

            public static string GetAreaName(RouteData routeData)
            {
                object area;
                if (routeData.DataTokens.TryGetValue("area", out area))
                {
                    return area as string;
                }

                return GetAreaName(routeData.Route);
            }
        }

        #endregion
    }
}